1. 异步组件:让页面加载更灵活
1.1 异步组件的意义
异步组件的核心在于“异步”二字,即以异步方式加载和渲染组件。为什么要这么做?设想一个大型应用,包含数百个组件,如果所有组件都在页面初始化时同步加载,首屏加载时间会显著增加,用户体验变差。异步组件通过按需加载(lazy loading)解决了这个问题,尤其在以下场景中大放异彩:
- 代码分割:将应用拆分为多个小块,仅在需要时加载,减少初始加载的资源体积。
- 服务端组件下发:从服务器动态获取组件,适用于内容管理系统等场景。
- 优化首屏加载:只加载当前页面必需的组件,提升加载速度。
从原理上看,异步组件的实现并不依赖框架支持,开发者完全可以通过 JavaScript 的动态 import()
手动实现。例如:
// 同步加载
import App from './App.vue'
createApp(App).mount('#app')
// 异步加载
const loader = () => import('./App.vue')
loader().then(App => {
createApp(App).mount('#app')
})
上述代码展示了如何通过动态 import()
实现异步加载。然而,手动实现异步组件需要处理许多细节,比如加载失败、超时、占位内容等。Vue.js 3 提供了 defineAsyncComponent
函数,封装了这些复杂逻辑,让开发者更专注于业务开发。
1.2 异步组件要解决的问题
一个完善的异步组件需要考虑以下问题:
- 加载失败:如果组件加载失败,是否渲染一个错误提示组件?
- 加载中状态:组件加载时,是否展示占位内容(如 Loading 组件)?
- 加载速度差异:加载可能很快,也可能很慢,是否需要延迟展示 Loading 组件,避免快速加载时的闪烁?
- 重试机制:加载失败后,是否允许自动或手动重试?
Vue.js 3 的异步组件通过以下功能应对这些问题:
- 指定加载失败时渲染的错误组件(
errorComponent
)。
- 配置加载中的占位组件(
loadingComponent
)及其延迟时间(delay
)。
- 设置加载超时时长(
timeout
)。
- 提供加载失败后的重试机制。
这些功能的实现离不开 defineAsyncComponent
函数,接下来我们将深入探讨其实现原理。
1.3 异步组件的实现原理
1.3.1 基础实现:封装defineAsyncComponent
defineAsyncComponent
是一个高阶组件,接收一个加载器函数或配置对象,返回一个包装组件。这个包装组件负责根据加载状态渲染不同的内容。以下是其核心实现:
function defineAsyncComponent(loader) {
let InnerComp = null
return {
name: 'AsyncComponentWrapper',
setup() {
const loaded = ref(false)
loader().then(c => {
InnerComp = c
loaded.value = true
})
return () => {
return loaded.value ? { type: InnerComp } : { type: Text, children: '' }
}
}
}
}
关键点:
- 高阶组件:
defineAsyncComponent
返回一个包装组件,负责管理加载状态。
- 加载状态管理:通过
ref
创建 loaded
标志,跟踪组件是否加载成功。
- 占位内容:加载未完成时,渲染一个空文本节点作为占位,减少 DOM 操作开销。
使用方式非常简单:
import { defineAsyncComponent } from 'vue'
export default {
components: {
AsyncComp: defineAsyncComponent(() => import('./CompA.vue'))
}
}
在模板中,<AsyncComp />
就像普通组件一样使用,但背后却是异步加载。
1.3.2 处理超时与错误组件
网络请求的不确定性要求异步组件支持超时和错误处理。defineAsyncComponent
允许通过配置对象指定超时时长和错误组件。例如:
const AsyncComp = defineAsyncComponent({
loader: () => import('./CompA.vue'),
timeout: 2000,
errorComponent: MyErrorComp
})
实现中增加了超时逻辑和错误捕获:
function defineAsyncComponent(options) {
if (typeof options === 'function') {
options = { loader: options }
}
const { loader, timeout, errorComponent } = options
let InnerComp = null
return {
name: 'AsyncComponentWrapper',
setup() {
const loaded = ref(false)
const error = shallowRef(null)
const timeoutFlag = ref(false)
loader()
.then(c => {
InnerComp = c
loaded.value = true
})
.catch(err => {
error.value = err
})
let timer = null
if (timeout) {
timer = setTimeout(() => {
timeoutFlag.value = true
error.value = new Error(`Async component timed out after ${timeout}ms.`)
}, timeout)
}
onUnmounted(() => clearTimeout(timer))
const placeholder = { type: Text, children: '' }
return () => {
if (loaded.value) {
return { type: InnerComp }
} else if (error.value && errorComponent) {
return { type: errorComponent, props: { error: error.value } }
}
return placeholder
}
}
}
}
关键点:
- 错误捕获:通过
catch
捕获加载过程中的所有错误(网络失败、超时等),存储在 error
中。
- 超时处理:设置定时器,超时后创建错误对象并触发错误组件渲染。
- 错误组件 props:将错误对象作为
props
传递给 errorComponent
,便于开发者自定义错误处理逻辑。
- 清理定时器:组件卸载时通过
onUnmounted
清除定时器,避免内存泄漏。
1.3.3 优化体验:Loading 组件与延迟
网络状况不同,组件加载速度可能差异很大。如果加载很快,立即展示 Loading 组件可能导致闪烁,影响用户体验。为此,Vue.js 3 提供了 delay
和 loadingComponent
选项:
const AsyncComp = defineAsyncComponent({
loader: () => import('./CompA.vue'),
delay: 200,
loadingComponent: {
setup() {
return () => ({ type: 'h2', children: 'Loading...' })
}
}
})
实现中新增了加载状态管理:
function defineAsyncComponent(options) {
if (typeof options === 'function') {
options = { loader: options }
}
const { loader, delay, loadingComponent, timeout, errorComponent } = options
let InnerComp = null
return {
name: 'AsyncComponentWrapper',
setup() {
const loaded = ref(false)
const error = shallowRef(null)
const loading = ref(false)
let loadingTimer = null
if (delay) {
loadingTimer = setTimeout(() => {
loading.value = true
}, delay)
} else {
loading.value = true
}
loader()
.then(c => {
InnerComp = c
loaded.value = true
})
.catch(err => {
error.value = err
})
.finally(() => {
loading.value = false
clearTimeout(loadingTimer)
})
let timer = null
if (timeout) {
timer = setTimeout(() => {
error.value = new Error(`Async component timed out after ${timeout}ms.`)
}, timeout)
}
onUnmounted(() => {
clearTimeout(timer)
clearTimeout(loadingTimer)
})
const placeholder = { type: Text, children: '' }
return () => {
if (loaded.value) {
return { type: InnerComp }
} else if (error.value && errorComponent) {
return { type: errorComponent, props: { error: error.value } }
} else if (loading.value && loadingComponent) {
return { type: loadingComponent }
}
return placeholder
}
}
}
}
关键点:
- 加载状态:通过
loading
标志跟踪加载中状态。
- 延迟展示:通过
delay
设置定时器,仅当加载时间超过指定值时才展示 loadingComponent
。
- 清理定时器:无论加载成功或失败,都在
finally
中清理 loadingTimer
,避免 Loading 组件滞留。
- 卸载逻辑:修改
unmount
函数,确保 Loading 组件能正确卸载,递归处理组件的 subTree
。
1.3.4 重试机制
网络不稳定时,组件加载失败是常见问题。Vue.js 3 支持通过 onError
回调实现重试机制。以下是实现思路:
function defineAsyncComponent(options) {
if (typeof options === 'function') {
options = { loader: options }
}
const { loader, onError } = options
let InnerComp = null
let retries = 0
function load() {
return loader()
.catch(err => {
if (onError) {
return new Promise((resolve, reject) => {
const retry = () => {
resolve(load())
retries++
}
const fail = () => reject(err)
onError(retry, fail, retries)
})
} else {
throw err
}
})
}
return {
name: 'AsyncComponentWrapper',
setup() {
const loaded = ref(false)
const error = shallowRef(null)
const loading = ref(false)
load()
.then(c => {
InnerComp = c
loaded.value = true
})
.catch(err => {
error.value = err
})
.finally(() => {
loading.value = false
})
return () => {
if (loaded.value) {
return { type: InnerComp }
} else if (error.value && options.errorComponent) {
return { type: options.errorComponent, props: { error: error.value } }
} else if (loading.value && options.loadingComponent) {
return { type: options.loadingComponent }
}
return { type: Text, children: '' }
}
}
}
}
关键点:
- 重试逻辑:通过
load
函数封装加载逻辑,捕获错误后调用 onError
,将 retry
和 fail
函数交给用户。
- 重试计数:通过
retries
记录重试次数,供用户决定是否继续重试。
- 用户控制:用户通过
onError(retry, fail, retries)
决定是重试还是抛出错误。
使用示例:
defineAsyncComponent({
loader: () => import('./CompA.vue'),
onError(retry, fail, retries) {
if (retries < 3) {
retry()
} else {
fail()
}
}
})
2. 函数式组件:简单至上的选择
2.1 函数式组件的核心
函数式组件是一个普通函数,返回虚拟 DOM,特点是无状态、无生命周期,编写简单直观。在 Vue.js 2 中,函数式组件因性能优势被广泛使用,而在 Vue.js 3 中,由于有状态组件的初始化性能已大幅优化,函数式组件的主要优势在于其简单性。
定义一个函数式组件非常简单:
function MyFuncComp(props) {
return { type: 'h1', children: props.title }
}
MyFuncComp.props = {
title: String
}
关键点:
- 无状态:函数式组件不维护自身状态,仅依赖传入的
props
。
- 静态 props:通过在函数上定义
props
属性指定接收的参数类型。
2.2 函数式组件的实现原理
Vue.js 3 通过复用有状态组件的逻辑实现函数式组件。在 patch
函数中,通过检测 vnode.type
的类型区分组件类型:
function patch(n1, n2, container, anchor) {
const { type } = n2
if (typeof type === 'string') {
// 处理元素
} else if (typeof type === 'object' || typeof type === 'function') {
// 处理组件(对象为有状态组件,函数为函数式组件)
if (!n1) {
mountComponent(n2, container, anchor)
} else {
patchComponent(n1, n2, anchor)
}
}
}
在 mountComponent
函数中,针对函数式组件进行特殊处理:
function mountComponent(vnode, container, anchor) {
const isFunctional = typeof vnode.type === 'function'
let componentOptions = vnode.type
if (isFunctional) {
componentOptions = {
render: vnode.type,
props: vnode.type.props
}
}
// 继续有状态组件的初始化逻辑
}
关键点:
- 类型检测:通过
typeof vnode.type === 'function'
判断是否为函数式组件。
- 统一处理:将函数式组件的
type
作为 render
函数,props
作为选项,复用有状态组件的挂载逻辑。
- 性能优化:跳过不必要的数据初始化和生命周期钩子,提升初始化效率。
2.3 函数式组件的应用场景
函数式组件适用于以下场景:
- 简单展示组件:如纯展示的标题、图标等,无需状态管理。
- 性能敏感场景:虽然 Vue.js 3 中性能差距不大,但在复杂列表渲染中,函数式组件仍可减少少量开销。
- 代码简洁:适合快速开发原型或轻量组件。